跳到主要内容

Java 集合类学习 Map 集合

Map 集合接口​

Map 集合的特点:存储一对数据(Key-Value),无序、无下标,键不可重复,值可重复

不要把 Map 和 HashMap搞混了,Map 单纯是一种 Key-Value 形式的存储方式,HashMap 只是它最常用的实现类

Map 接口中的常用方法​

  • void clear():删除该 Map 对象中所有键值对;
  • boolean containsKey(Object key):查询 Map 中是否包含指定的 key 值;
  • boolean containsValue(Object value):查询 Map 中是否包含一个或多个 value;
  • Set entrySet():返回 map 中包含的键值对所组成的 Set 集合,每个集合都是 Map.Entry 对象。
  • Object get(): 返回指定key对应的value,如果不包含key则返回null;
  • boolean isEmpty():查询该Map是否为空;
  • Set keySet():返回Map中所有key组成的集合;
  • Collection values():返回该Map里所有value组成的Collection。
  • Object put(Object key,Object value):添加一个键值对,如果集合中的key重复,则覆盖原来的键值对;
  • void putAll(Map m):将Map中的键值对复制到本Map中;
  • Object remove(Object key):删除指定的key对应的键值对,并返回被删除键值对的value,如果不存在,则返回null;
  • boolean remove(Object key,Object value):删除指定键值对,删除成功返回true;
  • int size():返回该Map里的键值对个数;

HashMap​

最常用 的Map,HashMap 的值是没有顺序的,他是按照 key 的 HashCode 来实现的,就是根据 key 的 HashCode 值来存储数据,根据 key 可以直接获取它的 Value,同时它具有很快的访问速度。

HashMap 最多只允许一条记录的 key 值为 Null(多条会覆盖),但是允许多条记录的 Value 为 Null。

使用位桶和链表实现(jdk1.8 改用红黑树存储而非链表),它是线程不安全的 Map,方法上都没有 synchronize 关键字修饰

Hashtable​

与 HashMap 类似,不同的是 key 和 value 的值均不允许为 null;

Hashtable 是线程安全的一个 Map 实现类,它实现线程安全的方法是在各个方法上添加了 synchronize 关键字,即任一时刻只有一个线程能写 Hashtable,因此也导致了 Hashtale 在写入时会比较慢,且只有 Hashtable 是继承自 Dictionary,HashMap 和 TreeMap 都继承自 AbstractMap 抽象类,LinkedHashMap 继承自 HashMap。

但是现在已经不再推荐使用 HashTable 了,因为现在有了 ConcurrentHashMap 这个专门用于多线程场景下的 Map 实现类,其大大优化了多线程下的性能。

LinkedHashMap​

LinkedHashMap 它的特点主要在于 Linked,带有这个字眼的就表示底层用的是链表来进行的存储。相对于其他的无序的 Map 实现类,还有像 TreeMap 这样的排序类,LinkedHashMap 最大的特点在于有序,但是它的有序主要体现在先进先出 FIFO 上。没错, LinkedHashMap 主要依靠双向链表和 Hash 表来实现的。(其实就是保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的。不过在遍历的时候会比 HashMap 慢。但是 key 和 value 均允许为空)

使用示例:

public static void main(String[] args) {
LinkedHashMap<String, Temp> map = new LinkedHashMap<>();
map.put("张三", new Temp(18, "张三"));
map.put("张三02", new Temp(18, "张三"));
map.put("王二", new Temp(14, "王二"));
map.put("赵六", new Temp(12, "赵六"));
map.forEach((k, v) -> {
System.out.printf("key:%s: value:%s \n", k, v);
});
}

存储原理,设置了一个 EntryHeader 用来维持链表的顺序

发生 Hash 冲突时如何指向​

仔细看下图,这里虽然在计算 hashcode 时还是发生了 hash 冲突,采用链地址法解决了冲突,但是这里的 Entry 对象是采用双向链表保存的,即每个 Entry 都有一个 after 和 before 的属性。所以当插入一个 entry 时,如果发生了冲突,就可以将新的 Entry 插入 Entry 链表中的头部,但是按照双向链表的角度来说,又会将该 Entry 插入到双向链表的尾部。

ConcurrentHashMap​

这个 Map 实现类是在 jdk1.5 中加入的,其在 jdk1.6/1.7 中的主要实现原理是 segment 段锁,它不再使用和 HashTable 一样的 synchronize 一样的关键字对整个方法进行加锁,而是转而 利用 segment 段落锁 来对其进行加锁,以保证 Map 的多线程安全。

其实可以理解为,一个 ConcurrentHashMap 是由多个 HashTable 组成,所以它允许获取不用段锁的线程同时持有该资源,segment 有多少个,理论上就可以同时有多少个线程来持有它这个资源。

其默认的 segment 是一个数组,默认长度为 16。也就是说理论商可以提高 16 倍的性能。

TreeMap​

TreeMap 不允许 key 的值为 null,且它也是 SortedMap 这个接口的唯一实现类

TreeMap 也是一个很常用的 Map 实现类,因为他具有一个很大的特点就是会对 Key 进行排序,使用了 TreeMap 存储键值对,再使用 Iterator 进行遍历输出时,会发现其默认采用 key 由小到大的顺序输出键值对,如果想要按照其他的方式来排序,需要重写也就是 override 它的 compartor 接口。

使用示例:

public static void main(String[] args) {
// 这里 -o1.compareTo(o2) 表示倒序
TreeMap<String, Integer> map = new TreeMap<>((o1, o2) -> -o1.compareTo(o2));

map.put("key_1", 1);
map.put("key_2", 2);
map.put("key_3", 3);

Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println(key + ":" + map.get(key));
}
}

输出

key_3:3
key_2:2
key_1:1

另外,TreeMap 底层的存储结构也是一颗红黑树。因为红黑树查找效率高,只有 O(lgn)O(lgn) 。它是一种自平衡的二叉查找树。在每次插入和删除节点时,都可以自动调节树结构,以保证树的高度是 lgnlgn 。

WeakHashMap​

在 Map 家族中,WeakHashMap 是一个很特殊的成员,从名字上看与 HashMap 相关,但是与 HashMap 有着很大的差别,翻译成中文后表示弱 HashMap,俗称缓存 HashMap。

WeakHashMap 它是一个 “弱键”,它的 Key 值和 Value 都可以是 null,而且其 Map 中如果这个 Key 值指向的对象没被使用,此时触发了 GC,该对象就会被回收掉的。

其原理主要是使用的 WeakReference 和 ReferenceQueue 实现的,其 key 就是 weakReference,而 ReferenceQueue 中保存了被回收的 Key-Value。

如果当其中一个 Key-Value 不再使用被回收时,就将其加入 ReferenceQueue 队列中。当下次再次调用该 WeakHashMap 时,就会去更新该 map,比如 ReferenceQueue 中的 key-value,将其中包含的 key-value 全部删除掉。这就是所谓的 “自动删除”。

具体细节参考:【集合系列】- 深入浅出的分析 WeakHashMap

如何使 Map 线程安全?​

我们都知道很多 Map 是非线程安全的(非同步的)。那么怎么才能让这些 Map 变成线程安全的呢?

下面以 HashMap 为例:

synchronizedMap 是一个方法,HashMap 本身非线程安全的,但是当使用 Collections.synchronizedMap(new HashMap()) 进行包装后就返回一个线程安全的 Map。

点进这个 synchronizedMap 方法内部,可以发现它创建了一个 SynchronizedMap 对象

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}

来看 SynchronizedMap 的主要方法

public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}

从源码可以看出,SynchronizedMap 实现线程安全的方法也是比较简单的,所有方法都是先对锁对象 mutex 上锁,然后再直接调用 Map 类型成员变量 m 的相关方法。

这样一来,线程在执行方法时,只有先获得了 mutex 的锁才能对 m 进行操作。因此,跟 Hashtable 一样,在同一个时间点,只能有一个线程对 SynchronizedMap 对象进行操作,虽然保证了线程安全,却导致了性能低下。

它最大的好处就是传入的是一个 Map 接口,而不是具体的实现类,所以像 TreeMap 这种 Map 实现类就可以通过这个方法来生成一个线程安全的 Map

synchronizedMap(Map) 和 ConcurrentHashMap 区别 ?

ConcurrentHashMap 是 Java 1.5 中 Hashtable 的替代品,是并发包的一部分。使用 ConcurrentHashMap,不仅可以在并发多线程环境中安全地使用它,而且还提供比 Hashtable 和 synchronizedMap 更好的性能。

但是对于没有给出线程安全却需要线程安全的 Map 实现类就可以使用 synchronizedMap(Map) 来创建了

Reference​

【集合系列】- 深入浅出的分析 WeakHashMap java中Map有哪些实现类和使用场景